package com.ctrip.framework.apollo.configservice.controller;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.ctrip.framework.apollo.biz.entity.Release;
import com.ctrip.framework.apollo.common.entity.AppNamespace;
import com.ctrip.framework.apollo.configservice.service.AppNamespaceServiceWithCache;
import com.ctrip.framework.apollo.configservice.service.config.ConfigService;
import com.ctrip.framework.apollo.configservice.util.InstanceConfigAuditUtil;
import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

/**
 * @author Jason Song(song_s@ctrip.com)
 */
@RestController
@RequestMapping("/configs")
public class ConfigController {
	private static final Splitter X_FORWARDED_FOR_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();
	private final ConfigService configService;
	private final AppNamespaceServiceWithCache appNamespaceService;
	private final NamespaceUtil namespaceUtil;
	private final InstanceConfigAuditUtil instanceConfigAuditUtil;
	private final Gson gson;

	private static final Type configurationTypeReference = new TypeToken<Map<String, String>>() {
	}.getType();

	public ConfigController(final ConfigService configService, final AppNamespaceServiceWithCache appNamespaceService, final NamespaceUtil namespaceUtil, final InstanceConfigAuditUtil instanceConfigAuditUtil, final Gson gson) {
		this.configService = configService;
		this.appNamespaceService = appNamespaceService;
		this.namespaceUtil = namespaceUtil;
		this.instanceConfigAuditUtil = instanceConfigAuditUtil;
		this.gson = gson;
	}

	// clientSideReleaseKey: 客户端侧的Release Key ，用于和获得的Release的releaseKey对比，判断是否有配置更新
	// messagesAsString: 客户端当前请求的Namespace的通知消息明细，调用#transformMessages(messagesAsString)方法，解析messagesAsString参数，创建ApolloNotificationMessages对象
	@GetMapping(value = "/{appId}/{clusterName}/{namespace:.+}")
	public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,
			@PathVariable String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter,
			@RequestParam(value = "releaseKey", defaultValue = "-1") String clientSideReleaseKey,
			@RequestParam(value = "ip", required = false) String clientIp,
			@RequestParam(value = "messages", required = false) String messagesAsString, HttpServletRequest request,
			HttpServletResponse response) throws IOException {
		String originalNamespace = namespace;
		// 若Namespace名以.properties结尾，移除该结尾，并设置到ApolloConfigNotification中。例如application.properties => application
		namespace = namespaceUtil.filterNamespaceName(namespace);
		// 获得归一化的Namespace名字。因为，客户端Namespace会填写错大小写
		namespace = namespaceUtil.normalizeNamespace(appId, namespace);

		// 若clientIp未提交，从Request中获取
		if (Strings.isNullOrEmpty(clientIp)) {
			clientIp = tryToGetClientIp(request);
		}
		// 解析messagesAsString参数，创建ApolloNotificationMessages对象
		ApolloNotificationMessages clientMessages = transformMessages(messagesAsString);
		// 创建Release数组
		List<Release> releases = Lists.newLinkedList();

		String appClusterNameLoaded = clusterName;
		// 获得Namespace对应的Release对象
		if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
			// 获得Release对象
			Release currentAppRelease = configService.loadConfig(appId, clientIp, appId, clusterName, namespace, dataCenter, clientMessages);

			if (currentAppRelease != null) {
				releases.add(currentAppRelease);
				// we have cluster search process, so the cluster name might be overridden
				// 获得Release对应的Cluster名字
				appClusterNameLoaded = currentAppRelease.getClusterName();
			}
		}

		// 若Namespace为关联类型，则获取关联的Namespace的Release对象
		// if namespace does not belong to this appId, should check if there is a public configuration
		if (!namespaceBelongsToAppId(appId, namespace)) {
			Release publicRelease = this.findPublicConfig(appId, clientIp, clusterName, namespace, dataCenter,
					clientMessages);
			if (!Objects.isNull(publicRelease)) {
				releases.add(publicRelease);
			}
		}

		// 若获得不到Release，返回状态码为404的响应
		if (releases.isEmpty()) {
			response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("Could not load configurations with appId: %s, clusterName: %s, namespace: %s", appId, clusterName, originalNamespace));
			Tracer.logEvent("Apollo.Config.NotFound", assembleKey(appId, clusterName, originalNamespace, dataCenter));
			return null;
		}
		// 记录InstanceConfig
		auditReleases(appId, clusterName, dataCenter, clientIp, releases);

		// 计算Config Service的合并 ReleaseKey。 当有多个Release时，使用 "+" 作为字符串的分隔
		String mergedReleaseKey = releases.stream().map(Release::getReleaseKey).collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR));

		// 对比Client的合并Release Key。若相等，说明没有改变，返回状态码为302的响应
		if (mergedReleaseKey.equals(clientSideReleaseKey)) {
			// Client side configuration is the same with server side, return 304
			response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
			Tracer.logEvent("Apollo.Config.NotModified", assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));
			return null;
		}

		// 创建ApolloConfig对象
		ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace, mergedReleaseKey);
		// 合并Release的配置，并将结果设置到ApolloConfig中
		apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));

		Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));
		return apolloConfig;
	}

	private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
		// Every app has an 'application' namespace
		if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
			return true;
		}

		// if no appId is present, then no other namespace belongs to it
		if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
			return false;
		}

		AppNamespace appNamespace = appNamespaceService.findByAppIdAndNamespace(appId, namespaceName);

		return appNamespace != null;
	}

	/**
	 * @param clientAppId the application which uses public config
	 * @param namespace   the namespace
	 * @param dataCenter  the datacenter
	 */
	private Release findPublicConfig(String clientAppId, String clientIp, String clusterName, String namespace,
			String dataCenter, ApolloNotificationMessages clientMessages) {
		AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);

		// check whether the namespace's appId equals to current one
		if (Objects.isNull(appNamespace) || Objects.equals(clientAppId, appNamespace.getAppId())) {
			return null;
		}

		String publicConfigAppId = appNamespace.getAppId();

		return configService.loadConfig(clientAppId, clientIp, publicConfigAppId, clusterName, namespace, dataCenter,
				clientMessages);
	}

	/**
	 * Merge configurations of releases. Release in lower index override those in
	 * higher index
	 */
	Map<String, String> mergeReleaseConfigurations(List<Release> releases) {
		Map<String, String> result = Maps.newLinkedHashMap();
		for (Release release : Lists.reverse(releases)) {
			result.putAll(gson.fromJson(release.getConfigurations(), configurationTypeReference));
		}
		return result;
	}

	private String assembleKey(String appId, String cluster, String namespace, String dataCenter) {
		List<String> keyParts = Lists.newArrayList(appId, cluster, namespace);
		if (!Strings.isNullOrEmpty(dataCenter)) {
			keyParts.add(dataCenter);
		}
		return keyParts.stream().collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR));
	}

	private void auditReleases(String appId, String cluster, String dataCenter, String clientIp, List<Release> releases) {
		if (Strings.isNullOrEmpty(clientIp)) {
			// no need to audit instance config when there is no ip
			return;
		}
		for (Release release : releases) {
			instanceConfigAuditUtil.audit(appId, cluster, dataCenter, clientIp, release.getAppId(), release.getClusterName(), release.getNamespaceName(), release.getReleaseKey());
		}
	}

	private String tryToGetClientIp(HttpServletRequest request) {
		String forwardedFor = request.getHeader("X-FORWARDED-FOR");
		if (!Strings.isNullOrEmpty(forwardedFor)) {
			return X_FORWARDED_FOR_SPLITTER.splitToList(forwardedFor).get(0);
		}
		return request.getRemoteAddr();
	}

	ApolloNotificationMessages transformMessages(String messagesAsString) {
		ApolloNotificationMessages notificationMessages = null;
		if (!Strings.isNullOrEmpty(messagesAsString)) {
			try {
				notificationMessages = gson.fromJson(messagesAsString, ApolloNotificationMessages.class);
			} catch (Throwable ex) {
				Tracer.logError(ex);
			}
		}

		return notificationMessages;
	}
}
